Passed
Push — master ( 102ccc...71c79b )
by Barry
01:09
created

helpers.js ➔ jsonDelKeys   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 11
rs 9.4285
1
/*
2
=====fsnip======
3
4
fsnip is a command line utility to extract and modify json from a file.
5
6
*/
7
const fs = require('fs')
8
const jp = require('jsonpath')
9
const stringify = require('json-stringify-pretty-compact')
10
const chalk = require('chalk')
11
12
export function cli (args) {
13
  if (args.length === 3 && args[2] === '--help') {
14
    console.info('fsnip is a tool for extracting json snippets from json files.\n\n' +
15
                 'Usage:\n' + '' +
16
                 'TODO still \n' +
17
                 '  fsnip FILE [options [arguments]]    process the file and output the result to the console\n' +
18
                 '  FILE         Specifies the file to process.\n' +
19
                 '  --ellipsify  replaces the passed object with ellipses (...)\n' +
20
                 '                 but excludes any keys which follow prepended by ~\n' +
21
                 '                 eg. fsnip myfile.json --ellipsify $..address ~postcode')
22
  } else if (args.length >= 3) {
23
    try {
24
      var txt = fs.readFileSync(args[2]).toString()
25
    } catch (err) {
26
      console.error(chalk.redBright("unable to read file '" + args[2] + "'"))
27
    }
28
    if (typeof txt !== 'undefined') {
29
      console.info(fsnipDo(args.slice(3), txt))
30
    }
31
  } else { // we couldn't recognise the format on the command line
32
    console.error(chalk.redBright('Unrecognised arguments passed to fsnip. See fsnip --help'))
33
  }
34
}
35
36
function fsnipDo (cmdOpts, inputText) {
37
  // does the processing of the fsnip command
38
  // inputText is the text we want to modify
39
  if (cmdOpts === null || cmdOpts.length === 0) { return inputText } // no processing required as no options passed in
40
  var src = { // a temporary structure containing the text we are working on its type eg. 'json' (which is set later)
41
    text: inputText,
42
    type: '',
43
    outputOptions: {},
44
    error: [],
45
    json: null,
46
    plain: null
47
  }
48
49
  // now we are going to parse through the options and arguments to extract individual options together with their arguments
50
  var cmdOpt = '' // current option from the cmdOptsString list
51
  var cmdArgs = [] // array containing any arguments for the cmdOpt
52
  for (var i = 0; i < cmdOpts.length; i++) {
53
    if (cmdOpts[i].substr(0, 2) === '--') { // this is a new option eg. --ellipsify
54
      processOption()
55
      cmdOpt = cmdOpts[i] // store the new option we have found
56
      cmdArgs = [] // reset ready for any new arguments
57
    } else {
58
      // this must be an argument for the current option
59
      if (cmdOpt === '') { // error if we don't currently have an option
60
        src.error.push("invalid argument '" + cmdOpts[i] + "' passed without valid option to fsnip")
61
      } else {
62
        cmdArgs.push(cmdOpts[i])
63
      }
64
    }
65
  }
66
  processOption()
67
  postProcess(src)
68
  return src.error.length === 0 ? src.text : chalk.redBright(src.error)
69
  
70
  function processOption() {
71
    // process/run any option we've found
72
    if (cmdOpt !== '') {
73
      runOption(cmdOpt, cmdArgs, src)
74
    }
75
  }
76
}
77
78
function runOption (option, args, inpObj) {
79
  // option is a string eg. '-jsonEllipsify'
80
  // arguments is an array of arguments for the option
81
  // inpObj is an object containing the text, type and json object we need to modify
82
  // this function acts as a marsheller to identify options and process them accordingly
83
  let funcs = {
84
    '--json': () => { json(inpObj) },
85
    '--prettify': () => { jsonPrettify(inpObj, args) },
86
    '--ellipsify': () => { jsonEllipsify(inpObj, args) },
87
    '--snip': () => { jsonSnippet(inpObj, args) },
88
    '--delKeys': () => { jsonDelKeys(inpObj, args) },
89
    '--from': () => { textFrom(inpObj, args, false) },
90
    '--start': () => { textFrom(inpObj, args, true) },
91
    '--to': () => { textTo(inpObj, args, false) },
92
    '--finish': () => { textTo(inpObj, args, true) }
93
  }
94
95
  if (funcs[option]) {
96
    funcs[option]()
97
  } else {
98
    inpObj.error.push("invalid option '" + option + "' for fsnip")
99
  }
100
}
101
102
function postProcess (inpObj) {
103
  // does any post process tidying up
104
  if (inpObj.type === 'json') {
105
    // stringify as required
106
    let opts = inpObj.outputOptions
107
    if (opts.maxLength === 'infinity' && opts.margins === false) {
108
      inpObj.text = JSON.stringify(inpObj.json)
109
    } else if (opts.maxLength === 0 && opts.margins === false) {
110
      inpObj.text = JSON.stringify(inpObj.json, null, opts.indent)
111
    } else {
112
      inpObj.text = stringify(inpObj.json, opts)
113
    }
114
    // now replace any placeholders. The placeholders are valid JSON but what we replace them with may not be valid JSON
115
    inpObj.text = inpObj.text.replace(/\[\s*"fsnipPlaceholderArrEllipses"\s*\]/g, '[...]')
116
    inpObj.text = inpObj.text.replace(/\{\s*"fsnipPlaceholderObj"\s*:\s*"Ellipses"\s*\}/g, '{...}') // do this separately to the one below so that if the object is empty it appears all on one line
117
    inpObj.text = inpObj.text.replace(/"fsnipPlaceholderObj"\s*:\s*"Ellipses"/g, '...')
118
    inpObj.text = inpObj.text.replace(/"fsnipPlaceholderStrEllipses"/g, '"..."')
119
  } else if (inpObj.type === 'plain') {
120
    inpObj.text = inpObj.plain.trim()
121
  }
122
}
123
124
export function setInputType (inpObj, newType) { // only exported for testing purposes
125
  if (typeof inpObj.type === 'undefined' || inpObj.type === '') { // type has not previously been set
126
    inpObj.type = newType
127
    if (newType === 'json') {
128
      inpObj.json = JSON.parse(inpObj.text)
129
      jsonPrettify(inpObj) // sets default output options for json
130
      return true
131
    } else if (newType === 'plain') {
132
      inpObj.plain = inpObj.text
133
      return true
134
    } else {
135
      return false
136
    }
137
  } else if (inpObj.type !== newType) { // it's already been set to something else so there's a problem
138
    if (typeof inpObj.error === 'undefined') { inpObj.error = [] }
139
    inpObj.error.push('cannot mix options designed to process different types of file')
140
    return false
141
  } else {
142
    return true
143
  }
144
}
145
146
function buildJsonSearchPath (keyName) {
147
  if (keyName.substr(0, 1) === '$') {
148
    return keyName
149
  } else {
150
    return "$..['" + keyName + "']"
151
  }
152
}
153
154
function removeQuotes (str) {
155
  // if the passed string has matching encapsulating quotes these are removed
156
  if ((str.substr(0, 1) === '\'' && str.substr(-1) === '\'') ||
157
      (str.substr(0, 1) === '"' && str.substr(-1) === '"')) {
158
    return str.substr(1, str.length - 2)
159
  } else {
160
    return str
161
  }
162
}
163
164
// =================json===============================
165
function json (inpObj) {
166
    // cmdArgs is an array of arguments
167
    // json is an object containing the json object we need to modify
168
  setInputType(inpObj, 'json') // all we do is flag our content as being json
169
}
170
171
// =================jsonPrettify=======================
172
function jsonPrettify (inpObj, cmdArgs) {
173
  // cmdArgs is an (optional) array of arguments being indent, maxLength, margins
174
  // they are all passed as strings so need to be converted to numbers where appropriate
175
  // we use - 0 to convert string numbers to numeric and === against itself to check for NaN
176
  if (setInputType(inpObj, 'json')) {
177
    let opts = inpObj.outputOptions
178
    // set defaults
179
    opts.margins = false
180
    opts.maxLength = 45
181
    opts.indent = 2
182
    // overwrite with any values passed in
183
    if (cmdArgs !== undefined) {
184
      if ((cmdArgs[0] - 0) === (cmdArgs[0] - 0)) { opts.indent = (cmdArgs[0] - 0) }
185
      if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) { opts.maxLength = (cmdArgs[1] - 0) }
186
      if (cmdArgs[1] === 'infinity') { opts.maxLength = 'infinity' }
187
      opts.margins = (cmdArgs[2] === 'true') // defaults to false if margins anything other than true
188
    }
189
  }
190
}
191
192
// =================ellipsify==========================
193
function jsonEllipsify (inpObj, cmdArgs) {
194
  // cmdArgs is an array of arguments
195
  // json is an object containing the json object we need to modify
196
197
  if (setInputType(inpObj, 'json')) {
198
    // we have two types of argument for Ellipsify, plain and exclude so separate them out
199
    var cmdArgsPlain = []
200
    var cmdArgsExclude = []
201
    for (let i = 0; i < cmdArgs.length; i++) {
202
      if (cmdArgs[i].substr(0, 1) === '~') {
203
        cmdArgsExclude.push(removeQuotes(cmdArgs[i].substr(1)))
204
      } else {
205
        cmdArgsPlain.push(removeQuotes(cmdArgs[i]))
206
      }
207
    }
208
    if (cmdArgsPlain.length === 0) { cmdArgsPlain.push('$') }
209
    for (let i = 0; i < cmdArgsPlain.length; i++) {
210
      minimizeJsonProperty(inpObj.json, cmdArgsPlain[i], cmdArgsExclude)
211
    }
212
  }
213
}
214
215
export function minimizeJsonProperty (json, property, excludes) { // only exported for test purposes
216
  // this function takes a json object as input.and for every occurrence of the given property puts a placeholder
217
  // but only if it is an array or an object.
218
  var arrPlaceholder = ['fsnipPlaceholderArrEllipses'] // a valid json array used as a placeholder to be replaced later with [...] (which is not valid json)
219
  var strPlaceholder = 'fsnipPlaceholderStrEllipses'
220
  var jsonPaths = jp.paths(json, buildJsonSearchPath(property)) // creates an array of all the paths of instances of the the property we want to minimize
221
  for (var i = 0; i < jsonPaths.length; i++) {
222
    let jsonPath = jp.stringify(jsonPaths[i])
223
    switch (jp.value(json, jsonPath).constructor.name) {
224
      case 'Object':
225
        delKeys(json, jsonPath, excludes)
226
        jp.value(json, jsonPath)['fsnipPlaceholderObj'] = 'Ellipses' // add a placeholder for the Ellipses
227
        break
228
      case 'Array':
229
        jp.value(json, jsonPath, arrPlaceholder)
230
        break
231
      case 'String':
232
        jp.value(json, jsonPath, strPlaceholder)
233
        break
234
      default:
235
        // do nothing
236
    }
237
  }
238
239
  function delKeys (json, jsonPath, excludes) {
240
    var keys = Object.keys(jp.value(json, jsonPath))
241
    for (var j = 0; j < keys.length; j++) {
242
      if (excludes.indexOf(keys[j]) === -1) {
243
        // this key is not in the excludes list so we need to delete it
244
        delete jp.value(json, jsonPath)[keys[j]]
245
      }
246
    }
247
  }
248
}
249
250
// ===================snip Function==============================
251
function jsonSnippet (inpObj, cmdArgs) {
252
  // cmdArgs is an array of arguments
253
  // inpObj is an object containing the json object we need to modify
254
  // the format of the call is eg.
255
  // '--snip vessel 2' which would extract the second instance of "vessel" in the json supplied
256
  // with the instance identifier being optional
257
  if (setInputType(inpObj, 'json')) {
258
    var occ = 1
259
    if (cmdArgs.length === 1) {
260
      occ = 1 // by default we snip the first occurrence of this property
261
    } else if (cmdArgs.length === 2) {
262
      if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) {
263
        occ = (cmdArgs[1] - 0)
264
        if (occ < 1) {
265
          inpObj.error.push('--snip requires its second argument to be a numeric values of at least 1 being the instance required')
266
          return
267
        }
268
      } else {
269
        inpObj.error.push("--snip requires its second argument to be numeric eg. '--snip vessel 2' with the optional second argument being the instance required")
270
        return
271
      }
272
    } else {
273
      inpObj.error.push("--snip requires 1 or 2 arguments eg. '--snip vessel 2' with the optional second argument being the instance required.")
274
      return
275
    }
276
    var jsonPaths = jp.paths(inpObj.json, buildJsonSearchPath(removeQuotes(cmdArgs[0]))) // creates an array of all the paths to this property
277
    if (jsonPaths.length < occ) {
278
      inpObj.error.push('--snip failed because there were only ' + jsonPaths.length + " occurrences of '" + removeQuotes(cmdArgs[0]) + "' found.")
279
      return
280
    }
281
    inpObj.json = jp.value(inpObj.json, jp.stringify(jsonPaths[occ - 1]))
282
  }
283
}
284
285
// ===================delKeys Function===========================
286
function jsonDelKeys (inpObj, cmdArgs) {
287
  // cmdArgs is an array of arguments
288
  // inpObj is an object containing the json object we need to remove keys from
289
  // the format of the call is eg.
290
  // '-jsonDelKeys vessel gnss' which would delete all instances of "vessel" and "gnss" in the json supplied
291
  if (setInputType(inpObj, 'json')) {
292
    for (var i = 0; i < cmdArgs.length; i++) {
293
      deleteJsonKey(inpObj.json, removeQuotes(cmdArgs[i]))
294
    }
295
  }
296
}
297
298
function deleteJsonKey (json, key) {
299
  // deletes all occurrences of key within json
300
  var jsonPaths = jp.paths(json, buildJsonSearchPath(key)) // creates an array of all the paths of instances of the key we want to delete
301
  var parent
302
  for (var i = 0; i < jsonPaths.length; i++) {
303
    let jsonPath = jp.stringify(jsonPaths[i])
304
    parent = jp.parent(json, jsonPath)
305
    if (Array.isArray(parent)) {
306
      parent.splice(jsonPaths[i][jsonPaths[i].length - 1], 1)
307
    } else {
308
      delete parent[jsonPaths[i][jsonPaths[i].length - 1]]
309
    }
310
  }
311
}
312
313
// ===================textFrom=================================
314
function textFrom (inpObj, cmdArgs, inclusive) {
315
  // cmdArgs is an array of arguments
316
  // inpObj is an object containing the text object we need to snip contents from
317
  // the format of the call is eg.
318
  // '--textFrom "some text" 2 - would start from the second instance of "some text"
319
  if (setInputType(inpObj, 'plain')) {
320
    let x = findLocation(inpObj, cmdArgs, inclusive ? '--start' : '--from')
321
    if (x.found) {
322
      inpObj.plain = inpObj.plain.substr(x.loc + (inclusive === true ? 0 : x.len))
323
    }
324
  }
325
}
326
327
// ===================textTo===================================
328
function textTo (inpObj, cmdArgs, inclusive) {
329
  // cmdArgs is an array of arguments
330
  // inpObj is an object containing the text object we need to snip contents from
331
  // the format of the call is eg.
332
  // '--textTo "some text" 2 - would go up to the second instance of "some text"
333
  if (setInputType(inpObj, 'plain')) {
334
    let x = findLocation(inpObj, cmdArgs, inclusive ? '--finish' : '--to')
335
    if (x.found) {
336
      inpObj.plain = inpObj.plain.substring(0, x.loc + (inclusive === true ? x.len : 0))
337
    }
338
  }
339
}
340
341
function findLocation (inpObj, cmdArgs, errString) {
342
  // find the location of the nth occurrence of the text specified in the command arguments
343
  let occ
344
  if (cmdArgs.length === 1) {
345
    occ = 1 // by default we take from the first occurrence of this text
346
  } else if (cmdArgs.length === 2) {
347
    if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) {
348
      occ = (cmdArgs[1] - 0)
349
      if (occ < 1) {
350
        inpObj.error.push(errString + ' requires its second argument to be a numeric value of at least 1 being the instance required')
351
        return {found: false}
352
      }
353
    } else {
354
      inpObj.error.push(errString + " requires its second argument to be numeric eg. '" + errString + " sometext 2' with the optional second argument being the instance required")
355
      return {found: false}
356
    }
357
  } else {
358
    inpObj.error.push(errString + " requires 1 or 2 arguments eg. '" + errString + " sometext' with the optional second argument being the instance required.")
359
    return {found: false}
360
  }
361
  let x = -1
362
  let arg = removeQuotes(cmdArgs[0])
363
  for (let i = 0; i < occ; i++) {
364
    x = inpObj.plain.indexOf(arg, x + 1)
365
  }
366
  if (x === -1) {
367
    inpObj.error.push('unable to find occurrence ' + occ + ' of "' + arg + '"')
368
  }
369
  return {found: (x !== -1), loc: x, len: arg.length}
370
}
371